using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
using System.ComponentModel;
using XnaAux;

namespace AnimationPipline
{
    /// <summary>
    /// This class will be instantiated by the XNA Framework Content Pipeline
    /// to apply custom processing to content data, converting an object of
    /// type TInput to TOutput. The input and output types may be the same if
    /// the processor wishes to alter data without changing its type.
    ///
    /// This should be part of a Content Pipeline Extension Library project.
    ///
    /// TODO: change the ContentProcessor attribute to specify the correct
    /// display name for this processor.
    /// </summary>
    [ContentProcessor(DisplayName = "Animation Processor")]
    public class AnimationProcessor : ModelProcessor
    {
        #region Members
        ModelContent model;

        private const float TinyLength = 1e-8f;

        private const float TineCosAngle = 0.9999999f;

        /// <summary>
        /// Bones lookup table, converts bone names to indices
        /// </summary>
        private Dictionary<string, int> mBones = new Dictionary<string, int>();

        private string mCustomMaterialProcessor = "";
        
        private int section = 0;

        private bool skinned = false;

        private List<int> mSkelToBone = new List<int>();

        #endregion

        /// <summary>
        /// Indicate if this model is skinned
        /// </summary>
        [Browsable(true)]
        [DisplayName("Skinned")]
        [DefaultValue(false)]
        public bool Skinned { get { return skinned; } set { skinned = value; } }

        /// <summary>
        /// A custom material processor to use instead of the default one
        /// </summary>
        [Browsable(true)]
        [DisplayName("Custom Material Processor")]
        public string CustomMaterialProcessor
        {
            get{ return mCustomMaterialProcessor;}
            set { mCustomMaterialProcessor = value; }
        }

        /// <summary>
        /// The ship section
        /// </summary>
        [Browsable(true)]
        [DisplayName("Ship Section")]
        public int Section { get { return section; } set { section = value; } }


        public override ModelContent Process(NodeContent input, ContentProcessorContext context)
        {
            if (skinned)
            {
                ProcessSkeleton(input);
            }
            model = base.Process(input, context);

            AnimationClips lClips = ProcessAnimations(model, input, context);
            lClips.SkelToBone = mSkelToBone;
            model.Tag = lClips;

            return model;
        }

        private AnimationClips ProcessAnimations(ModelContent aModel,
                                    NodeContent aInput, ContentProcessorContext aContext)
        {
            // First build a lookup table so we can determine the
            // index into the list of bones from a bone name.
            for (int i = 0; i < aModel.Bones.Count; i++)
            {
                mBones[aModel.Bones[i].Name] = i;
            }
            AnimationClips lAnimationClips = new AnimationClips();

            ProcessAnimationsRecursive(aInput, lAnimationClips);

            // Ensure all animations have a first keyframe for every bone
            foreach( AnimationClips.Clip lClip in lAnimationClips.Clips.Values)
            {
                for ( int b = 0; b < mBones.Count; b++)
                {
                    List<AnimationClips.Keyframe> lKeyframes = lClip.Keyframes[b];
                    if(lKeyframes.Count == 0 ||lKeyframes[0].Time > 0)
                    {
                        AnimationClips.Keyframe lKeyframe = new AnimationClips.Keyframe();
                        lKeyframe.Time = 0;

                        Matrix lTransform = model.Bones[b].Transform;
                        lTransform.Right = Vector3.Normalize(lTransform.Right);
                        lTransform.Up = Vector3.Normalize(lTransform.Up);
                        lTransform.Backward = Vector3.Normalize(lTransform.Backward);

                        lKeyframe.Rotation = Quaternion.CreateFromRotationMatrix(lTransform);
                        lKeyframe.Translation = lTransform.Translation;

                        lKeyframes.Insert(0, lKeyframe);
                    }
                }
            }

            return lAnimationClips;
        }

        /// <summary>
        /// Revursive function that processes the entire scene graph, collecting up,
        /// all of the animation data
        /// </summary>
        /// <param name="aInput">The input scene graph node</param>
        /// <param name="aAnimationClips">The animation clips object we put animation in</param>
        private void ProcessAnimationsRecursive(NodeContent aInput, AnimationClips aAnimationClips)
        {
            foreach (KeyValuePair<string, AnimationContent> lAnimation in aInput.Animations)
            {
                // Do we have this animation before?

                AnimationClips.Clip lClip;
                if (!aAnimationClips.Clips.TryGetValue(lAnimation.Key, out lClip))
                {
                    // Never before seen clip
                    System.Diagnostics.Trace.WriteLine("New clip: " + lAnimation.Key);

                    lClip = new AnimationClips.Clip();
                    lClip.Name = lAnimation.Key;
                    lClip.Duration = lAnimation.Value.Duration.TotalSeconds;
                    lClip.Keyframes = new List<AnimationClips.Keyframe>[mBones.Count];
                    for (int b = 0; b < mBones.Count; b++)
                        lClip.Keyframes[b] = new List<AnimationClips.Keyframe>();

                    aAnimationClips.Clips[lAnimation.Key] = lClip;
                }
                else if (lAnimation.Value.Duration.TotalSeconds > lClip.Duration)
                {
                    lClip.Duration = lAnimation.Value.Duration.TotalSeconds;
                }

                //
                // For each channel, determine the bone and then process all of the
                // keyframes for that bone.
                //

                foreach (KeyValuePair<string, AnimationChannel> lChannel in lAnimation.Value.Channels)
                {
                    // What is the bone index?
                    int lBoneIndex;

                    if (!mBones.TryGetValue(lChannel.Key, out lBoneIndex))
                        continue;       // Ignore if not a named bone

                    if (!skinned)// && UselessAnimationTest(lBoneIndex))
                    {
                        continue;
                    }
                    foreach (AnimationKeyframe lKeyframe in lChannel.Value)
                    {
                        Matrix lTransform = lKeyframe.Transform;    // Keyframe Transformation

                        AnimationClips.Keyframe lNewKeyframe = new AnimationClips.Keyframe();
                        lNewKeyframe.Time = lKeyframe.Time.TotalSeconds;

                        lTransform.Right = Vector3.Normalize(lTransform.Right);
                        lTransform.Up = Vector3.Normalize(lTransform.Up);
                        lTransform.Backward = Vector3.Normalize(lTransform.Backward);
                        lNewKeyframe.Rotation = Quaternion.CreateFromRotationMatrix(lTransform);
                        lNewKeyframe.Translation = lTransform.Translation;

                        //LinearKeyframeReduction(ref lClip.Keyframes[lBoneIndex]);
                        lClip.Keyframes[lBoneIndex].Add(lNewKeyframe);
                    }
                }
            }
            foreach (NodeContent lChild in aInput.Children)
            {
                ProcessAnimationsRecursive(lChild, aAnimationClips);
            }
        }

        bool UselessAnimationTest(int aBoneId)
        {
            foreach (ModelMeshContent lMesh in model.Meshes)
            {
                if (lMesh.ParentBone.Index == aBoneId)
                    return false;
            }
            return true;
        }

        private void LinearKeyframeReduction(ref List<AnimationClips.Keyframe> aKeyframes)
        {
            if (aKeyframes.Count < 3)
                return;
            for (int i = 1; i < aKeyframes.Count - 1; )
            {
                AnimationClips.Keyframe a = aKeyframes[i - 1];
                AnimationClips.Keyframe b = aKeyframes[i];
                AnimationClips.Keyframe c = aKeyframes[i + 1];

                // Determine the nodes before and after the current node.

                float t = (float)((b.Time - a.Time) /
                            (c.Time - b.Time));

                Vector3 lTranslation = Vector3.Lerp(a.Translation, c.Translation, t);
                Quaternion lRotation = Quaternion.Slerp(a.Rotation, c.Rotation, t);

                if ((lTranslation - b.Translation).LengthSquared() < TinyLength &&
                    Quaternion.Dot(lRotation, b.Rotation) > TineCosAngle)
                {
                    aKeyframes.Remove(b);
                    //mDeletedCount++;
                }
                i++;
            }

        }

        /// <summary>
        /// Use our custom material processor
        /// to convert selected materials in this model.
        /// </summary>
        /// <param name="material"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        protected override MaterialContent ConvertMaterial(MaterialContent material, ContentProcessorContext context)
        {
            if (CustomMaterialProcessor == "")
                return base.ConvertMaterial(material, context);

            OpaqueDataDictionary processorParameters = new OpaqueDataDictionary();
            processorParameters.Add("Section", section);
            processorParameters.Add("Skinned", skinned);
            return context.Convert<MaterialContent, MaterialContent>(material,
                                  CustomMaterialProcessor,
                                  processorParameters);
        }

        /// <summary>
        /// Process the skeleton in support of skeletal animation...
        /// </summary>
        /// <param name="input"></param>
        private BoneContent ProcessSkeleton(NodeContent input)
        {
            // Find the skeleton.
            BoneContent skeleton = MeshHelper.FindSkeleton(input);

            if (skeleton == null)
                throw new InvalidContentException("Input skeleton not found.");

            // We don't want to have to worry about different parts of the model being
            // in different local coordinate systems, so let's just bake everything.
            FlattenTransforms(input, skeleton);

            //
            // 3D Studio Max includes helper bones that end with "Nub"
            // These are not part of the skinning system and can be 
            // discarded.  TrimSkeleton removes them from the geometry.
            //

            TrimSkeleton(skeleton);

            //Convert the heirarchy of nodes and bones into a list
            List<NodeContent> lNodeBones = FlattenHeirarchy(input);
            IList<BoneContent> lSkelBones = MeshHelper.FlattenSkeleton(skeleton);

            // Create a dictionary to convert a node to an index into the array of node bones
            Dictionary<NodeContent, int> lNodeToIndex = new Dictionary<NodeContent, int>();
            for (int i = 0; i < lNodeBones.Count; i++)
            {
                lNodeToIndex[lNodeBones[i]] = i;
            }

            // Now create the array that maps the bones to the nodes
            foreach (BoneContent lSkelBone in lSkelBones)
            {
                mSkelToBone.Add(lNodeToIndex[lSkelBone]);
            }
            return skeleton;
        }

         /// <summary>
         /// Bakes unwanted transforms into the model geometry,
         /// so everything ends up in the same coordinate system.
         /// </summary>
         void FlattenTransforms(NodeContent node, BoneContent skeleton)
         {
             foreach (NodeContent child in node.Children)
             {
                 // Don't process the skeleton, because that is special.
                 if (child == skeleton)
                     continue;

                 // Bake the local transform into the actual geometry.
                 MeshHelper.TransformScene(child, child.Transform);

                 // Having baked it, we can now set the local
                 // coordinate system back to identity.
                 child.Transform = Matrix.Identity;

                 // Recurse.
                 FlattenTransforms(child, skeleton);
             }
         
        }

         /// <summary>
         /// 3D Studio Max includes an extra help bone at the end of each
         /// IK chain that doesn't effect the skinning system and is 
         /// redundant as far as the game is concerned.  This function
         /// looks for children who's name ends with "Nub" and removes
         /// them from the heirarchy.
         /// </summary>
         /// <param name="skeleton">Root of the skeleton tree</param>
         void TrimSkeleton(NodeContent skeleton)
         {
             List<NodeContent> todelete = new List<NodeContent>();

             foreach (NodeContent child in skeleton.Children)
             {
                 if (child.Name.EndsWith("Nub") || child.Name.EndsWith("Footsteps"))
                     todelete.Add(child);
                 else
                     TrimSkeleton(child);
             }

             foreach (NodeContent child in todelete)
             {
                 skeleton.Children.Remove(child);
             }
         }


         /// <summary>
         /// Convert a tree of nodes into a list of nodes in topological order.
         /// </summary>
         /// <param name="item">The root of the heirarchy</param>
         /// <returns></returns>
         private List<NodeContent> FlattenHeirarchy(NodeContent item)
         {
             List<NodeContent> nodes = new List<NodeContent>();
             nodes.Add(item);
             foreach (NodeContent child in item.Children)
             {
                 FlattenHeirarchy(nodes, child);
             }

             return nodes;
         }

         private void FlattenHeirarchy(List<NodeContent> nodes, NodeContent item)
         {
             nodes.Add(item);
             foreach (NodeContent child in item.Children)
             {
                 FlattenHeirarchy(nodes, child);
             }
         }
    }
}